index.page.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import type React from 'react';
  2. import type { JSX, ReactNode } from 'react';
  3. import { useEffect } from 'react';
  4. import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
  5. import dynamic from 'next/dynamic';
  6. import Head from 'next/head';
  7. import { isClient } from '@growi/core/dist/utils';
  8. // biome-ignore-start lint/style/noRestrictedImports: no-problem lazy loaded components
  9. import { DescendantsPageListModalLazyLoaded } from '~/client/components/DescendantsPageListModal';
  10. import { ConflictDiffModalLazyLoaded } from '~/client/components/PageEditor/ConflictDiffModal';
  11. import { DrawioModalLazyLoaded } from '~/client/components/PageEditor/DrawioModal';
  12. import { HandsontableModalLazyLoaded } from '~/client/components/PageEditor/HandsontableModal';
  13. import { LinkEditModalLazyLoaded } from '~/client/components/PageEditor/LinkEditModal';
  14. import { TagEditModalLazyLoaded } from '~/client/components/PageTags/TagEditModal';
  15. import { TemplateModalLazyLoaded } from '~/client/components/TemplateModal';
  16. // biome-ignore-end lint/style/noRestrictedImports: no-problem lazy loaded components
  17. import { BasicLayout } from '~/components/Layout/BasicLayout';
  18. import { PageView } from '~/components/PageView/PageView';
  19. import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
  20. import { useEditorModeClassName } from '~/services/layout/use-editor-mode-class-name';
  21. import { useCurrentPageData, useCurrentPagePath } from '~/states/page';
  22. import { useHydratePageAtoms } from '~/states/page/hydrate';
  23. import { useRendererConfig } from '~/states/server-configurations';
  24. import {
  25. useSetupGlobalSocket,
  26. useSetupGlobalSocketForPage,
  27. } from '~/states/socket-io';
  28. import { useSetEditingMarkdown } from '~/states/ui/editor';
  29. import type { NextPageWithLayout } from '../_app.page';
  30. import { useHydrateBasicLayoutConfigurationAtoms } from '../basic-layout-page/hydrate';
  31. import { getServerSideCommonEachProps } from '../common-props';
  32. import { useInitialCSRFetch } from '../general-page';
  33. import { useHydrateGeneralPageConfigurationAtoms } from '../general-page/hydrate';
  34. import { registerPageToShowRevisionWithMeta } from '../general-page/superjson';
  35. import {
  36. detectNextjsRoutingType,
  37. NextjsRoutingType,
  38. } from '../utils/nextjs-routing-utils';
  39. import { useCustomTitleForPage } from '../utils/page-title-customization';
  40. import { mergeGetServerSidePropsResults } from '../utils/server-side-props';
  41. import { NEXT_JS_ROUTING_PAGE } from './consts';
  42. import {
  43. getServerSidePropsForInitial,
  44. getServerSidePropsForSameRoute,
  45. } from './server-side-props';
  46. import type { EachProps, InitialProps } from './types';
  47. import { useSameRouteNavigation } from './use-same-route-navigation';
  48. import { useShallowRouting } from './use-shallow-routing';
  49. // call superjson custom register
  50. registerPageToShowRevisionWithMeta();
  51. // biome-ignore-start lint/style/noRestrictedImports: no-problem dynamic import
  52. const GrowiContextualSubNavigation = dynamic(
  53. () => import('~/client/components/Navbar/GrowiContextualSubNavigation'),
  54. { ssr: false },
  55. );
  56. const GrowiPluginsActivator = dynamic(
  57. () =>
  58. import(
  59. '~/features/growi-plugin/client/components/GrowiPluginsActivator'
  60. ).then((mod) => mod.GrowiPluginsActivator),
  61. { ssr: false },
  62. );
  63. const DisplaySwitcher = dynamic(
  64. () =>
  65. import('~/client/components/Page/DisplaySwitcher').then(
  66. (mod) => mod.DisplaySwitcher,
  67. ),
  68. { ssr: false },
  69. );
  70. const PageStatusAlert = dynamic(
  71. () =>
  72. import('~/client/components/PageStatusAlert').then(
  73. (mod) => mod.PageStatusAlert,
  74. ),
  75. { ssr: false },
  76. );
  77. const UnsavedAlertDialog = dynamic(
  78. () => import('~/client/components/UnsavedAlertDialog'),
  79. { ssr: false },
  80. );
  81. const EditablePageEffects = dynamic(
  82. () =>
  83. import('~/client/components/Page/EditablePageEffects').then(
  84. (mod) => mod.EditablePageEffects,
  85. ),
  86. { ssr: false },
  87. );
  88. // biome-ignore-end lint/style/noRestrictedImports: no-problem dynamic import
  89. type Props = EachProps | InitialProps;
  90. const isInitialProps = (props: Props): props is InitialProps => {
  91. return (
  92. 'isNextjsRoutingTypeInitial' in props && props.isNextjsRoutingTypeInitial
  93. );
  94. };
  95. const Page: NextPageWithLayout<Props> = (props: Props) => {
  96. // Initialize Jotai atoms with initial data - must be called unconditionally
  97. const pageData = isInitialProps(props) ? props.pageWithMeta?.data : undefined;
  98. const pageMeta = isInitialProps(props) ? props.pageWithMeta?.meta : undefined;
  99. useHydratePageAtoms(pageData, pageMeta, {
  100. redirectFrom: props.redirectFrom ?? undefined,
  101. templateTags: props.templateTagData,
  102. templateBody: props.templateBodyData,
  103. });
  104. const currentPage = useCurrentPageData();
  105. const currentPagePath = useCurrentPagePath();
  106. const rendererConfig = useRendererConfig();
  107. const setEditingMarkdown = useSetEditingMarkdown();
  108. // setup socket.io
  109. useSetupGlobalSocket();
  110. useSetupGlobalSocketForPage();
  111. // Use custom hooks for navigation and routing
  112. useSameRouteNavigation();
  113. useShallowRouting(props);
  114. // If initial props and skipSSR, fetch page data on client-side
  115. useInitialCSRFetch(isInitialProps(props) && props.skipSSR);
  116. useEffect(() => {
  117. // Initialize editing markdown only when page path changes
  118. if (currentPagePath) {
  119. setEditingMarkdown(currentPage?.revision?.body || '');
  120. }
  121. }, [currentPagePath, currentPage?.revision?.body, setEditingMarkdown]);
  122. // If the data on the page changes without router.push, pageWithMeta remains old because getServerSideProps() is not executed
  123. // So preferentially take page data from useSWRxCurrentPage
  124. const pagePath = currentPagePath ?? props.currentPathname;
  125. const title = useCustomTitleForPage(pagePath);
  126. return (
  127. <>
  128. <Head>
  129. <title>{title}</title>
  130. </Head>
  131. <div className="dynamic-layout-root justify-content-between">
  132. <GrowiContextualSubNavigation currentPage={currentPage} />
  133. <PageView
  134. className="d-edit-none"
  135. pagePath={pagePath}
  136. rendererConfig={rendererConfig}
  137. />
  138. <EditablePageEffects />
  139. <DisplaySwitcher />
  140. <PageStatusAlert />
  141. </div>
  142. </>
  143. );
  144. };
  145. const BasicLayoutWithEditor = ({
  146. children,
  147. }: {
  148. children?: ReactNode;
  149. }): JSX.Element => {
  150. const editorModeClassName = useEditorModeClassName();
  151. return <BasicLayout className={editorModeClassName}>{children}</BasicLayout>;
  152. };
  153. type LayoutProps = Props & {
  154. children?: ReactNode;
  155. };
  156. const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
  157. // Hydrate sidebar atoms with server-side data - must be called unconditionally
  158. const initialProps = isInitialProps(props) ? props : undefined;
  159. useHydrateBasicLayoutConfigurationAtoms(
  160. initialProps?.searchConfig,
  161. initialProps?.sidebarConfig,
  162. initialProps?.userUISettings,
  163. );
  164. useHydrateGeneralPageConfigurationAtoms(
  165. initialProps?.serverConfig,
  166. initialProps?.rendererConfig,
  167. );
  168. return <BasicLayoutWithEditor>{children}</BasicLayoutWithEditor>;
  169. };
  170. Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
  171. // Get drawioUri from rendererConfig atom to ensure consistency across navigations
  172. const DrawioViewerScriptWithAtom = (): JSX.Element => {
  173. const rendererConfig = useRendererConfig();
  174. return <DrawioViewerScript drawioUri={rendererConfig.drawioUri} />;
  175. };
  176. return (
  177. <>
  178. <GrowiPluginsActivator />
  179. <DrawioViewerScriptWithAtom />
  180. <Layout {...page.props}>{page}</Layout>
  181. <UnsavedAlertDialog />
  182. <DescendantsPageListModalLazyLoaded />
  183. <DrawioModalLazyLoaded />
  184. <HandsontableModalLazyLoaded />
  185. <TemplateModalLazyLoaded />
  186. <LinkEditModalLazyLoaded />
  187. <TagEditModalLazyLoaded />
  188. <ConflictDiffModalLazyLoaded />
  189. </>
  190. );
  191. };
  192. export const getServerSideProps: GetServerSideProps<Props> = async (
  193. context: GetServerSidePropsContext,
  194. ) => {
  195. //
  196. // STAGE 1
  197. //
  198. const commonEachPropsResult = await getServerSideCommonEachProps(
  199. context,
  200. NEXT_JS_ROUTING_PAGE,
  201. );
  202. // Handle early return cases (redirect/notFound)
  203. if (
  204. 'redirect' in commonEachPropsResult ||
  205. 'notFound' in commonEachPropsResult
  206. ) {
  207. return commonEachPropsResult;
  208. }
  209. const commonEachProps = await commonEachPropsResult.props;
  210. // Handle redirect destination from common props
  211. if (commonEachProps.redirectDestination != null) {
  212. return {
  213. redirect: {
  214. permanent: false,
  215. destination: commonEachProps.redirectDestination,
  216. },
  217. };
  218. }
  219. //
  220. // STAGE 2
  221. //
  222. // detect Next.js routing type
  223. const nextjsRoutingType = detectNextjsRoutingType(
  224. context,
  225. NEXT_JS_ROUTING_PAGE,
  226. );
  227. // Merge all results in a type-safe manner (using sequential merging)
  228. return mergeGetServerSidePropsResults(
  229. commonEachPropsResult,
  230. nextjsRoutingType === NextjsRoutingType.INITIAL
  231. ? await getServerSidePropsForInitial(context)
  232. : await getServerSidePropsForSameRoute(context),
  233. );
  234. };
  235. export default Page;